Skip to content

fix: strip DeepSeek DSML tool-call markers from assistant content - #805

Open
sharadvc wants to merge 1 commit into
lsdefine:mainfrom
sharadvc:fix/dsml-marker-stripping
Open

sharadvc wants to merge 1 commit into
lsdefine:mainfrom
sharadvc:fix/dsml-marker-stripping

Conversation

@sharadvc

Copy link
Copy Markdown

Problem

DeepSeek's DSML protocol injects tool-call routing markers into the content stream before the actual tool_calls delta arrives. These markers (e.g. <‖DSML‖tool_calls>, <‖DSML‖invoke ...>) leak into the UI when tool calls fail or aren't parsed in time.

Fix

Adds _strip_dsml_markers() — a fast regex filter applied to every content chunk yielded by _parse_openai_sse().

  • Zero overhead on non-DeepSeek streams: the guard short-circuits on chunks that don't contain 'DSML'
  • Catches all known marker variants: full-width , Unicode , ASCII pipe |, and double-escaped forms
  • Applied at both the streaming yield point (real-time UI) and the final block assembly (fallback)

Testing

  • Verified with synthetic SSE chunks containing <‖DSML‖tool_calls>, <‖DSML‖invoke name="ShellCommand" arguments="{...}"> — all stripped cleanly
  • Non-DeepSeek streams unaffected (guard short-circuits)

Closes #804

DeepSeek's DSML protocol injects tool-call routing markers (e.g.
<‖DSML‖tool_calls>, <‖DSML‖invoke ...>) into the content stream
before the actual tool_calls delta arrives.  These markers leak into
the UI when tool calls fail or are not parsed in time.

This adds _strip_dsml_markers() — a fast regex filter applied to every
content chunk yielded by _parse_openai_sse().  The guard short-circuits
on chunks that don't contain 'DSML' so there is zero overhead on
non-DeepSeek streams.

Closes lsdefine#804
@sharadvc

Copy link
Copy Markdown
Author

Friendly bump — DeepSeek DSML strip is ready for review when you have a moment. Happy to tweak if needed.

@ZhulongNT

Copy link
Copy Markdown
Contributor

I tested the current PR implementation against DSML strings observed in real model_responses. Two gaps remain:

  1. Double full-width-pipe markers are only partially removed
_strip_dsml_markers('<||DSML||parameter name="x">abc</||DSML||parameter>')
# current result: '<abc</'
# expected:       'abc'

The |DSML| regex starts matching at the second pipe of ||DSML||, leaving the outer < / </ behind.

  1. Markers split across SSE chunks are not removed
chunks = ['hello<‖DS', 'ML‖tool_calls>world']
''.join(_strip_dsml_markers(x) for x in chunks)
# current result: 'hello<‖DSML‖tool_calls>world'

Because each chunk is filtered independently and the fast guard requires "DSML" in text, a marker split at any point inside DSML bypasses the filter. This is especially relevant for streaming, and the PR description explicitly mentions chunk-split regression tests.

Suggested direction: maintain a short carry-over buffer/state across SSE chunks, and match the double-pipe variants before their single-pipe substrings. Please also add regression tests for opening/closing parameter tags and every split position within a marker.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] DeepSeek DSML tool-call markers leak into assistant content and streamlit UI

2 participants